/+
Library dbapi - Simple database api for the D programming language.

Copyright (C) 2007 Myron Alexander (myron.alexander...gmail.com)

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+/

module dbapi.internal.sqlite3.cursor;

import std.boxer;
import std.stdarg;
import std.string : find, split, atoi;

import dbapi.base;

import dbapi.internal.sqlite3.definitions;
import dbapi.internal.sqlite3.internalconnection;
import dbapi.internal.sqlite3.statement;

import dbapi.internal.chk_struc_ok_for_fetch;

/* Required for "chk_struc_ok_for_fetch".
 */
import std.metastrings : ToString;
import std.traits : FieldTypeTuple;

/+
public class Cursor {
   this (InternalConnection iconn) {
   }
}
+/

public class Cursor {

   private InternalConnection m_iconn;

   private Statement  m_currentstmt;

   /* True when a row is available for fetching otherwise false.
    */
   private bool       m_rowAvailable;

   /* True when the statement has been executed but before the first
    * fetch.
    */
   private bool       m_newExecution;

   /* The number of rows returned as an array for fetchmany and the array growth
    * value for fetchall.
    */
   private int        m_arraysize = 10;


   //---------------------------------------------------------------------------
   // Constructors
   //---------------------------------------------------------------------------


   public this (InternalConnection iconn) {
      m_iconn = iconn;
   }


   public ~this () {
      close ();
   }


   //---------------------------------------------------------------------------
   // Interface properties
   //
   //    arraysize   (RW)
   //    connection  (R)
   //    description (R)
   //    lastrowid   (R)
   //    rowcount    (R)
   //---------------------------------------------------------------------------

   public int arraysize () {
      checkClosed ();
      return m_arraysize;
   }


   public int arraysize (int value) {
      checkClosed ();
      if (value <= 0) {
         throw (new SqlInterfaceException ("Invalid cursor arraysize value"))
            .setLongMsg (
               "Invalid cursor arraysize value specified. The size must be a "
               "non-zero, positive, value. Value specified "
               "($(RequestedArraySize)).")
            .setProperty ("RequestedArraySize", value);
      }

      return m_arraysize = value;
   }


   public ColumnAttributes[] description () {
      checkClosed ();

      /* When the no statement has been executed, return null.
       */
      if (m_currentstmt is null) return null;

      /* Request the number of columns that the statement will return.
       */
      int colcount = m_currentstmt.getStmtColCount ();

      /* If the statement is of a type that does not return a value, then
       * return null.
       */
      if (colcount == 0) return null;

      ColumnAttributes[] attributes = new ColumnAttributes[colcount];

      for (int i = 0; i < colcount; i++) {
         ColumnAttributes attr;
         attr.name = m_currentstmt.getStmtColName (i);
         fillTypeFromStmtColType (
               toupper (m_currentstmt.getStmtColType (i)), attr);
         attributes [i] = attr;
      }

      return attributes;
   }


   public int lastrowid () {
      assert (false, "Not implemented yet");
      checkClosed ();
   }


   public int rowcount ()
   in {
      assert (false, "rowcount property not implemented yet.");
   }
   body {
      checkClosed ();
      return -1;
   }


   //---------------------------------------------------------------------------
   // Interface methods
   //---------------------------------------------------------------------------


   public void execute (char[] operation, ...) {

      checkClosed ();

      createStatement (operation);

      int providedBindParmsCnt = _arguments.length;
      int expectedBindParmsCnt = m_currentstmt.numBindParameters;

      if (expectedBindParmsCnt > 0) {
         if (providedBindParmsCnt < expectedBindParmsCnt) {
            throw (new SqlProgrammingException (
               "Cannot execute statement as there are parameters of the SQL "
               "statement for which no value have been provided. Expecting (" ~
               str (expectedBindParmsCnt) ~ ") parameter values but were "
               "provided (" ~ str (providedBindParmsCnt) ~
               ") values."))
            .setSql (operation);

         } else if (providedBindParmsCnt > expectedBindParmsCnt) {
            throw (new SqlProgrammingException (
               "Cannot execute statement as the number of parameter values "
               "exceeds the number of parameters. Expecting (" ~
               str (expectedBindParmsCnt) ~ ") parameter values but were "
               "provided (" ~ str (providedBindParmsCnt) ~
               ") values."))
            .setSql (operation);
         }

         bindParameters (_arguments, _argptr);
      }

      m_iconn.transModeHandler.startExecute (m_currentstmt.statementType);

      {
         /* If the statement execute fails, the transaction handler error
          * method must be called. This block is done in it's own scope level
          * so that the scope guard will only execute if the step fails and not
          * if the endExecute fails.
          */
         scope (failure) m_iconn.transModeHandler.executionError ();

         /* Execute the statement. If the statement is a query that results in
          * data, the step() function will return true.
          */
         m_rowAvailable = m_currentstmt.step ();
      }

      m_iconn.transModeHandler.endExecute ();

      m_newExecution = true;
   }


   private void createStatement (char[] operation) {
      if (m_currentstmt is null) {
         m_currentstmt = new Statement (m_iconn, operation);

      } else if (m_currentstmt.isClosed () || m_currentstmt.text != operation) {
         delete m_currentstmt;
         m_currentstmt = new Statement (m_iconn, operation);

      } else {
         m_currentstmt.reset ();
      }
   }


   /**
    * Execute a batch update operation on given values.
    *
    * This method can be called with two different signatures:
    *
    *   executemany ("...", Box[]...)
    *     - Each box array is taken as a row with multiple values.
    *     - The number of values within the Box[] must exactly correlate with
    *       the number of parameters in the SQL. The only clarification for this
    *       is that, for named parameters, there must be one value per name but
    *       that name may appear more than once in the SQL.
    *     - The value types supported is the same as for execute.
    *
    *   executemany ("...", ...)
    *     - Each parameter is taken as a single value row.
    *     - The SQL must have exactly one positional, or named, parameter.
    *     - The value types supported is the same as for execute.
    *     - You can also pass in single value boxes mixed with other parameters.
    *       This allows you to use values from a prior query without unboxing
    *       them.
    *
    * The method calls executemany (operation, Box[][]) so the operation and
    * result will be the same.
    */
   int[] executemany (char[] operation, ...) {

      checkClosed ();

      if (0 == _arguments.length) {
         throw (new SqlProgrammingException (
            "Invalid number of parameter values. Zero parameter values were "
            "specified but an executemany operation requires at least one "
            "parameter."))
         .setSql (operation);
      }

      Box[][] values;

      if (_arguments[0] == typeid (Box[])) {
         //
         // Create multiple value row box array.
         //

         debug {writefln ("Creating array of box[]");}

         values = new Box[][_arguments.length];

         foreach (i, t; _arguments) {
            if (typeid (Box[]) != t) {
               throw (new SqlProgrammingException (
                  "Invalid bind type. Mixing single and multiple value rows. "
                  "The first argument was a Box[], thus it is assumed that "
                  "the rest of the arguments are multiple value rows but "
                  "found a non Box[] argument."))
               .setSql (operation)
               .setProperty ("ValueRow", i)
               .setProperty ("ValueType", t);
            }

            values[i] = va_arg!(Box[])(_argptr);
         }

      } else {
         //
         // Create single value row box array.
         //

         debug {writefln ("Creating array of other");}

         va_list data = _argptr;

         values = new Box[][](_arguments.length, 1);

         foreach (i, t; _arguments) {
            if (typeid (Box[]) == t) {
               throw (new SqlProgrammingException (
                  "Invalid bind type. Mixing single and multiple value rows. "
                  "The first argument type is other than Box[], thus it is "
                  "assumed that the rest of the arguments are single value "
                  "rows."))
               .setSql (operation)
               .setProperty ("ValueRow", i)
               .setProperty ("ValueType", t);

            } else if (typeid (Box) == t) {
               values[i][0] = *cast(Box*)data;

            } else {
               values[i][0] = box (t, data);
            }

            data = nextArg (t, data);
         }
      }

      return executemanyImpl (operation, values);
   }


   int[] executemany (char[] operation, Box[][] values)
   body {

      checkClosed ();

      //
      //
      //

      /* Validate that a non-null array was passed in.
       */
      if (values is null) {
         throw (new SqlProgrammingException (
            "Invalid parameter value array. The reference passed in is null "
            "but a populated array is required."))
         .setSql (operation);
      }

      /* Validate that there is at least one parameter row.
       */
      if (0 == values.length) {
         throw (new SqlProgrammingException (
            "Invalid number of parameter values. Zero parameter values were "
            "specified but an executemany operation requires at least one "
            "parameter."))
         .setSql (operation);
      }

      //
      //
      //

      return executemanyImpl (operation, values);
   }



   private int[] executemanyImpl (char[] operation, Box[][] values)
   in {
      assert (operation.length > 0);
      assert (values.length > 0);
   }
   body {

      createStatement (operation);

      //
      //
      //

      if (StatementType.STATEMENT_MODIFY != m_currentstmt.statementType) {
         throw new SqlProgrammingException (
            "Invalid statement type. The only permittable statements for "
            "batch operation are those that modify data such as INSERT, "
            "UPDATE, DELETE, and REPLACE. Error on statement (" ~ operation ~
            ").");
      }

      int expectedBindParmsCnt = m_currentstmt.numBindParameters;

      if (0 == expectedBindParmsCnt) {
         throw new SqlProgrammingException (
            "Invalid number of parameters specified. Zero parameters were "
            "specified in the SQL but an executemany operation requires at "
            "least one parameter. Error on statement (" ~ operation ~ ").");
      }

      //
      //
      //

      /* Validate that all the parameter rows are non-null and that the required
       * number of parameter values are present.
       */
      foreach (i, v; values) {
         if (v is null) {
            throw new SqlProgrammingException (
               "Invalid parameter value array. The value array for row (" ~
               str (i) ~ ") is null but a populated row array is required. "
               "Error on statement (" ~ operation ~ ").");

         } else if (expectedBindParmsCnt != v.length) {
            throw new SqlProgrammingException (
               "Invalid parameter value array. The number of parameter values "
               "do not match the number of parameters. Expecting (" ~
               str (expectedBindParmsCnt) ~ ") values but received (" ~
               str (v.length) ~ "). Error on parameter row (" ~ str (i) ~
               ") for statement (" ~ operation ~ ").");
         }

         foreach (j, w; v) {
            if (w.type is null) {
               throw new SqlProgrammingException (
                  "Invalid parameter value array. Found uninitialised box "
                  "value. Error on parameter row (" ~ str (i) ~
                  "), column (" ~ str (j) ~ "), for statement (" ~ operation ~
                  ").");
            }
         }
      }

      //
      //
      //

      m_iconn.transModeHandler.startBatch ();

      //
      //
      //

      int[] recordsAffected = new int[values.length];

      foreach (i, pa; values) {

         bool hasrow;

         try {
            m_currentstmt.reset ();
            bindParameters (pa);
            hasrow = m_currentstmt.step ();

         } catch (SqlException e) {
            m_iconn.transModeHandler.batchError ();
            recordsAffected[i] = BatchResult.EXECUTE_FAILED;
            throw new BatchUpdateException (e, recordsAffected[0..i+1]);
         }

         assert (!hasrow,
            "Queries are not allowed as batch statements. This should have "
            "been check before the statement is executed.");

         recordsAffected[i] = m_currentstmt.numChanges ();
      }


      //
      //
      //

      m_iconn.transModeHandler.endBatch ();

      //
      //
      //

      return recordsAffected;
   }


   private bool moveCursorToNextAvailableRecord () {

      checkClosed ();

      /* If no row is available then exit now.
       */
      if (!m_rowAvailable) return false;

      /* On execute, the statement is compiled and the first step is performed
       * so no step operation must be done on a newly executed statement.
       */
      if (m_newExecution) {
         m_newExecution = false;

      } else {
         //TODO - Add call to executeError of transaction handler and ensure
         //       that the original exception is not lost.
         m_rowAvailable = m_currentstmt.step ();
      }

      return m_rowAvailable;
   }


   public Box[] fetchone () {

      checkClosed ();

      /* Move the cursor to the next unread result row. If no row is
       * available then exit now.
       */
      if (!moveCursorToNextAvailableRecord ()) return null;

      int colcount = m_currentstmt.getRsColCount ();

      /* When the last step operation returned a record but the column count is
       * zero, something unexpected is happening so throw an exception.
       */
      if (0 == colcount) {
         m_rowAvailable = false;
         throw new SqlInterfaceException (
            "Unable to fetch a record. The operation to retrieve the next data "
            "row reported a row available but the number of columns reported "
            "is zero. This should never happen and is either a bug in this "
            "inteface, a bug in the SQLite implementation, or the way the "
            "SQLite implementation works has changed. Either way, unable to "
            "continue. ");
      }

      Box[] row = new Box[colcount];

      for (int i = 0; i < colcount; i++) {
         row [i] = m_currentstmt.getValue (i);
      }

      return row;
   }


   public Box[][] fetchmany (int size = -1)
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      checkClosed ();

      if (-1 != size && 0 >= size) {
         throw new SqlInterfaceException (
            "Invalid size for fetchmany. The size parameter must be either -1, "
            "to use arraysize, or a number greater than zero.");
      }

      if (-1 == size) {
         size = m_arraysize;
      }

      Box[][] result;

      Box[] row = fetchone ();

      /* Create and populate the array if a row is available.
       */
      if (row !is null) {

         result = new Box[][](size);

         int i = 0;

         /* Fill the array with available rows.
          */
         while (row !is null) {
            result[i++] = row;
            if (i == size) break;
            row = fetchone ();
         }

         if (i < size) {
            /* The value of i will be one greater than the index of the last row
             * due to the i++ when assigning the row thus the value of i is the
             * actual number of rows inserted into the array.
             */
            result.length = i;
         }
      }

      return result;
   }


   public Box[][] fetchall ()
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      checkClosed ();

      Box[][] result;

      int i = 0;
      Box[] row = fetchone ();

      while (row !is null) {
         if (i % m_arraysize == 0) {
            result.length = result.length + m_arraysize;
         }
         result[i++] = row;
         row = fetchone ();
      }

      if (result.length != i) {
         /* The value of i will be one greater than the index of the last row
          * due to the i++ when assigning the row thus the value of i is the
          * actual number of rows inserted into the array.
          */
         result.length = i;
      }

      return result;
   }


   /**
    * Move the cursor to the next unread row, or, when a row is unavailable,
    * return false.
    *
    * When the method returns true you can retrieve the row with fetchrow.
    * This method is intended for use with fetchrow and fetchstruct, do not
    * mix this method with fetchone as that method will also cause the cursor
    * to move.
    *
    * Returns:
    *    true when a row is available, otherwise false.
    *
    */
   public bool next () {
      return moveCursorToNextAvailableRecord ();
   }


   /**
    * Read only property returns true when the last database step
    * returned a row.
    *
    * In almost all cases, when this property is true, next() will return true.
    * The exception is after a call to fetchmanyrows, or fetchmanystructs, when
    * the last array element corresponds to the last row of the query. In that
    * case, the fetch loop will exit as the array is full but the database
    * step is not performed so the cursor pointer is pointing to the last
    * row instead of after the last row; then a call to next() will execute
    * a database step and return false.
    */
   public bool rowAvailable () {
      return m_rowAvailable;
   }


   /**
    * Type aware fetch from the database.
    *
    * With thanks to Chris Nicholson-Sauls, Lutger Blijdestijn,
    * and Christian Kam who provided code and information on how to make this
    * possible.
    *
    * Thanks to Kirk McDonald for the type aware fetch idea.
    */
   public Row!(T) fetchrow(T...) () {

      checkClosed ();

      if (!rowAvailable ()) {
         throw new SqlProgrammingException ("No data available");
      }

      Row!(T) r;

      int colcount = m_currentstmt.getRsColCount ();

      /* When the last step operation returned a record but the column count is
       * zero, something unexpected is happening so throw an exception.
       */
      if (0 == colcount) {
         m_rowAvailable = false;
         throw new SqlInterfaceException (
            "Unable to fetch a record. The operation to retrieve the next data "
            "row reported a row available but the number of columns reported "
            "is zero. This should never happen and is either a bug in this "
            "inteface, a bug in the SQLite implementation, or the way the "
            "SQLite implementation works has changed. Either way, unable to "
            "continue. ");

      } else if (colcount != r.c.length) {
         throw new SqlProgrammingException (
            "Number of types specified does not match the number of columns "
            "read from the query result.");
      }

      /* Retrieve one value for each type in the expression tuple and populate
       * the expression tuple.
       */
      foreach (i, v; r.c) {
         Box dbval = m_currentstmt.getValue (i);
         r.c[i] = unboxNullable!(typeof(r.c[i])) (dbval);
         r.nullFlag[i] = isNull (dbval);
      }

      return r;
   }


   public Row!(T)[] fetchmanyrows(T...) (int size = -1)
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      checkClosed ();

      if (-1 != size && 0 >= size) {
         throw new SqlInterfaceException (
            "Invalid size for fetchmanyrows. The size parameter must be either "
            "-1, to use arraysize, or a number greater than zero.");
      }

      if (-1 == size) {
         size = m_arraysize;
      }

      Row!(T)[] result;

      /* Create the result array if a row is available.
       */
      if (next ()) {

         result = new Row!(T)[](size);

         int i = 0;

         result[i++] = fetchrow!(T) ();

         while (i < size && next ()) {
            result[i++] = fetchrow!(T) ();
         }

         /* When the index (i) is less than the array size, reduce the array
          * size to the index value as the index value is the number of
          * rows populated in the result.
          */
         if (i < size) {
            /* The value of i will be one greater than the index of the last row
             * due to the i++ when assigning the row thus the value of i is the
             * actual number of rows inserted into the array.
             */
            result.length = i;
         }
      }

      return result;
   }


   public Row!(T)[] fetchallrows(T...) ()
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      checkClosed ();

      Row!(T)[] result;

      int i = 0;

      while (next ()) {
         if (i % m_arraysize == 0) {
            result.length = result.length + m_arraysize;
         }
         result[i++] = fetchrow!(T) ();
      }

      /* When the index (i) is less than the array size, reduce the array
       * size to the index value as the index value is the number of
       * rows populated in the result.
       */
      if (i < result.length) {
         /* The value of i will be one greater than the index of the last row
          * due to the i++ when assigning the row thus the value of i is the
          * actual number of rows inserted into the array.
          */
         result.length = i;
      }

      return result;
   }


   /**
    * Return a type aware record from the database and populate the struct.
    *
    * With thanks to Chris Nicholson-Sauls, Lutger Blijdestijn, and
    * Christian Kam.
    */
   T fetchstruct(T) () {

      checkClosed ();

      static if (is (T == struct)) {

         /* Validate the structure for the fetch operation.
          */
         mixin (chkStrucOkForFetch !("T"));

         return fetchstructImpl!(T);

      } else {
         static assert (0, "Invalid type, must be a struct.");
      }
   }


   /**
    * Return a type aware record from the database and populate the struct.
    *
    * Separated out for the multi row fetchs so that the structure checks can be
    * done once
    */
   private T fetchstructImpl(T) () {

      T t;

      /* Check if last struct field is a bool array of correct size to store
       * null flags.
       */
      static if (is (typeof (t.tupleof[$-1]) == tNullFlag[t.tupleof.length-1])) {
         const bool hasNullFlags = true;
      } else {
         const bool hasNullFlags = false;
      }

      /* When the last field is a bool array of the correct size to store
       * null flags, exclude from the type tuple.
       */
      static if (hasNullFlags) {
         auto r = fetchrow!(FieldTypeTuple!(T)[0..length-1])();

      } else {
         auto r = fetchrow!(FieldTypeTuple!(T))();
      }

      /* Populate the structure from the fetched data.
       */
      foreach (i, v; r.c) {
         // According to Christian, using v may not work every time.
         t.tupleof[i] = r.c[i];
      }

      /* If the structure contains nullFlags, populate it.
       */
      static if (hasNullFlags) {

         foreach (i, nullFlagValue; r.nullFlag) {
            /* I use the name of the field instead of tupleof to guarantee
             * the name so helper functions can be written.
             */
            t.nullFlag[i] = cast(tNullFlag)nullFlagValue;
         }
      }

      return t;
   }


   public T[] fetchmanystructs(T) (int size = -1)
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      /* Type passed in must be a structure.
       */
      static if (!is (T == struct)) {
         static assert (0, "Invalid type, must be a struct.");
      }

      /* Validate the structure for the fetch operation.
       */
      mixin (chkStrucOkForFetch !("T"));

      checkClosed ();

      if (-1 != size && 0 >= size) {
         throw new SqlInterfaceException (
            "Invalid size for fetchmanystructs. The size parameter must be "
            "either -1, to use arraysize, or a number greater than zero.");
      }

      if (-1 == size) {
         size = m_arraysize;
      }

      T[] result;

      /* Create the result array if a row is available.
       */
      if (next ()) {

         result = new T[](size);

         int i = 0;

         result[i++] = fetchstructImpl!(T);

         while (i < size && next ()) {
            result[i++] = fetchstructImpl!(T);
         }

         /* When the index (i) is less than the array size, reduce the array
          * size to the index value as the index value is the number of
          * rows populated in the result.
          */
         if (i < size) {
            /* The value of i will be one greater than the index of the last row
             * due to the i++ when assigning the row thus the value of i is the
             * actual number of rows inserted into the array.
             */
            result.length = i;
         }
      }

      return result;
   }


   public T[] fetchallstructs(T) ()
   in {
      assert (m_arraysize > 0,
         "It should not be possible to set m_arraysize to zero.");
   }
   body {

      /* Type passed in must be a structure.
       */
      static if (!is (T == struct)) {
         static assert (0, "Invalid type, must be a struct.");
      }

      /* Validate the structure for the fetch operation.
       */
      mixin (chkStrucOkForFetch !("T"));

      checkClosed ();

      T[] result;

      int i = 0;

      while (next ()) {
         if (i % m_arraysize == 0) {
            result.length = result.length + m_arraysize;
         }
         result[i++] = fetchstructImpl!(T);
      }

      /* When the index (i) is less than the array size, reduce the array
       * size to the index value as the index value is the number of
       * rows populated in the result.
       */
      if (i < result.length) {
         /* The value of i will be one greater than the index of the last row
          * due to the i++ when assigning the row thus the value of i is the
          * actual number of rows inserted into the array.
          */
         result.length = i;
      }

      return result;
   }



   public void close () {
      if (m_currentstmt !is null) {
         delete m_currentstmt;
      }
   }


   //---------------------------------------------------------------------------
   // Private methods
   //---------------------------------------------------------------------------


   private bool connectionOpen () {
      if (!m_iconn.isOpen ()) {
         close ();
         return false;
      } else {
         return true;
      }
   }


   public void printRawAttributes () {

      int colcount = m_currentstmt.getRsColCount ();

      for (int i = 0; i < colcount; i++) {
         writefln (
            "%s ::> %d", m_currentstmt.getStmtColName (i),
            m_currentstmt.getRsColType (i));
      }
   }


   /**
    * Extract the type name, precision and scale from the type declaration
    * string.
    *
    * The declaration string is an exact copy of the text used to create the
    * table column including all whitespace. Expected template is as follows:
    *
    *   type ::= typename | typename (number) | typename (number,number)
    *
    * The first bracket found must be the start of precision.
    *
    * Notes:
    *   - SQLite places no restriction on the type name so this could be
    *     anything.
    *   - SQLite does not check that the scale fits within the precision thus
    *     you could have a scale greater or equal to the precision.
    */
   private void fillTypeFromStmtColType (
         char[] stmtColType, inout ColumnAttributes attr) {

      bool found (int idx) {return idx != -1;}

      //writefln ("COLTYPE ==> %s", stmtColType);

      int idxBracketStrt = find (stmtColType, '(');
      int idxBracketEnd  = find (stmtColType, ')');

      if (found (idxBracketStrt)) {
         if (!found (idxBracketEnd)) {
            throw new SqlInterfaceException (
               "Unable to process column type string '"~stmtColType~"' as "
               "encountered start bracket but no end bracket. This is "
               "unexpected and may indicate that the SQLite database is "
               "corrupt, or the engine is corrupt, or the engine column type "
               "function has changed. Unable to continue.");
         }

         char[][] restrictionStr =
            split (stmtColType[(idxBracketStrt+1) .. idxBracketEnd], ",");

         attr.typeCode  = strip (stmtColType[0 .. idxBracketStrt]);
         attr.precision = atoi (strip (restrictionStr[0]));
         if (restrictionStr.length > 1)
            attr.scale = atoi (strip (restrictionStr[1]));

      } else {

         attr.typeCode = strip (stmtColType);

      }
   }

   //---------------------------------------------------------------------------
   // Section: Parameter binding
   //---------------------------------------------------------------------------

   private void bindParameters (TypeInfo[] _arguments, va_list _argptr) {
      Box[] values = boxArray (_arguments, _argptr);
      bindParameters (values);
   }


   private void bindParameters (Box[] values) {

      /* Clear the current bindings on a statement so if it was reused, and
       * the user forgot to bind a value to a parameter, that parameter would
       * have the default (NULL) value rather than the previous bound
       * value. If this is a new statement, then the clearBindings will have no
       * effect.
       */
      m_currentstmt.clearBindings ();

      TypeInfo t = typeofNestedBox (values[0]);

      if (t == typeid (NamedParameter)) {
         bindNamedParameters (values);

      } else {
         bindPositionalParameters (values);
      }
   }


   /**
    * For nested boxes, retrieve the type of the boxed value.
    *
    * Returns:
    *    Type of the first non-box value.
    */
   private TypeInfo typeofNestedBox (Box value) {

      return nestedBox (value).type;
   }


   /**
    * For nested boxes, retrieve the innermost box.
    *
    * Returns:
    *    The innermost box value.
    */
   private Box nestedBox (Box value) {

      Box v = value;

      while (v.type == typeid (Box)) {
         v = unbox!(Box)(v);
      }

      return v;
   }


   private void bindPositionalParameters (Box[] values) {

      foreach (i, value; values) {

         /* A value may be contained within nested boxes. This could be for
          * many reasons but the one I envision is the case where the value is
          * retrieved by a query. Thus we allow the developer to copy a box
          * returned by the fetch* statement to the execute.
          */
         Box v = nestedBox (value);

         if (v.type == typeid (NamedParameter)) {

            throw new SqlProgrammingException (
               "Invalid bind type. A named parameter instance was specified "
               "when positional binding is being performed. Mixing of named "
               "and positional binding is invalid. Error on parameter (" ~
               str (i) ~ "). SQL (" ~ m_currentstmt.text ~ ").");

         }

         bindParameter (i, v);
      }
   }


   private void bindNamedParameters (Box[] values) {

      foreach (i, v; values) {

         /* When binding with named parameters, nested boxes are not allowed;
          * only type NamedParameters are allowed.
          */
         if (v.type != typeid (NamedParameter)) {

            if (v.type == typeid (Box)) {
               throw new SqlProgrammingException (
                  "Invalid bind type. Nested boxes are not allowed when named "
                  "binding is being performed. Error on parameter (" ~
                  str (i) ~ "). SQL (" ~ m_currentstmt.text ~ ").");

            } else {
               throw new SqlProgrammingException (
                  "Invalid bind type. A non-named parameter instance was "
                  "specified when named binding is being performed. Mixing of "
                  "named and positional binding is invalid. Error on parameter (" ~
                  str (i) ~ "). SQL (" ~ m_currentstmt.text ~ ").");
            }
         }

         NamedParameter p = unbox!(NamedParameter)(v);

         assert (p.value.type != typeid (NamedParameter),
            "A named parameter cannot have a named parameter as a value. "
            "Check that the constructor/opCall for NamedParameter enforces "
            "this assumption.");

         assert (p.value.type != typeid (Box),
            "A named parameter cannot have a box for a value. "
            "Check that the constructor/opCall for NamedParameter unnests "
            " boxed values.");

         bindParameter (p.name, p.value);
      }
   }


   private void bindParameter (char[] name, Box value) {
      int i = m_currentstmt.parmIndexFromName (name);
      bindParameter (i, value);
   }


   private void bindParameter (int i, Box value)
   in {
      assert (value.type != typeid (Box),
         "Value box may not contain nested boxes.");
   }
   body {
      alias value v;

      if (v.type == typeid (int)) {
         m_currentstmt.bindInt (i, unbox!(int)(v));

      } else if (v.type == typeid (long)) {
         m_currentstmt.bindLong (i, unbox!(long)(v));

      } else if (v.type == typeid (char[])) {
         m_currentstmt.bindUtf8 (i, unbox!(char[])(v));

/+
   Removed until I can determine how to work with UTF16 data in SQLite in the
   interface.
      } else if (v.type == typeid (wchar[])) {
         m_currentstmt.bindUtf16 (i, unbox!(wchar[])(v));
+/
      } else if (v.type == typeid (void*)) {
         if (isNull (v)) {
            m_currentstmt.bindNull (i);

         } else {
            throw new SqlProgrammingException (
               "Invalid bind value. Received non-null ptr type void* but "
               "only support void* value of null.");
         }

      } else if (v.type == typeid (void[])) {
         m_currentstmt.bindBlob (i, unbox!(void[])(v));

      } else if (v.type == typeid (float)) {
         m_currentstmt.bindDouble (i, unbox!(float)(v));

      } else if (v.type == typeid (double)) {
         m_currentstmt.bindDouble (i, unbox!(double)(v));

      } else {
         throw new SqlProgrammingException (
            "Invalid bind type. Received type that cannot be bound. Only "
            "types supported are [null, int, long, char[], wchar[], float, "
            "double, void[] (blob)]. Received type (" ~ v.type.toString() ~
            ").");
      }
   }
   
   
   private void checkClosed () {
      if (!m_iconn.isOpen ()) {
         throw (new SqlClosedException (
            "Cursor operation not permitted on closed connection"))
            .setLongMsg (
               "Cursor operation not permitted on closed connection $(DSN).")
            .setDsn (m_iconn.dsn);
      }
   }
}

private va_list nextArg (TypeInfo t, va_list argptr) {
   return argptr + argumentLength (t.tsize());
}

/** Return the length of an argument in bytes. */
private size_t argumentLength (size_t baseLength)
{
   return (baseLength + int.sizeof - 1) & ~(int.sizeof - 1);
}


/**
 * Batch update exception composits underlying cause with array of update counts.
 *
 * The update counts are for all commands that were executed successfully during
 * the batch update, that is, all commands that were executed before the error
 * occurred. The order of elements in an array of update counts corresponds to
 * the order in which commands were added to the batch. The last element is the
 * iteration of the error and will contain the value BatchResult.EXECUTE_FAILED.
 */
class BatchUpdateException : SqlException {

   this (Exception cause, int[] updateCounts)
   in {
      assert (cause !is null);
      assert (updateCounts !is null);
      assert (updateCounts.length > 0);
      assert (updateCounts[$] == BatchResult.EXECUTE_FAILED);
   }
   body {
      super (null);
      m_cause        = cause;
      m_updateCounts = updateCounts;
   }


   override char[] toString () {
      return m_cause.toString ();
   }


   //~ override char[] sqlState () {

      //~ if (cast(SqlDatabaseException)m_cause) {
         //~ return (cast(SqlDatabaseException)m_cause).sqlState;

      //~ } else {
         //~ /* When the cause is not a SqlException, return the default value
          //~ * as set by the super constructor.
          //~ */
         //~ return null;
      //~ }
   //~ }


   //~ override int vendorCode () {

      //~ if (cast(SqlDatabaseException)m_cause) {
         //~ return (cast(SqlDatabaseException)m_cause).vendorCode;

      //~ } else {
         //~ /* When the cause is not a SqlException, return the default value
          //~ * as set by the super constructor.
          //~ */
         //~ return 0;
      //~ }
   //~ }


   int[] updateCounts () {
      return m_updateCounts;
   }


   private Exception m_cause;
   private int[]     m_updateCounts;
}
